<?php
/**
 * Product: ASW.Database.
 * Date: 2023-09-10
 * Time: 15:12
 */

namespace ASW\Database;

use ASW\Utility\Logging\Logger;
use PDO;
use PDOStatement;

class DbConnection
{
    public function __construct(private readonly string $dsn, private readonly ?string $username = null, private readonly ?string $password = null, private readonly ?array $options = null)
    {

    }

    private ?PDO  $_conn            = null;
    private array $_pdoParams       = [];
    private int   $_queryRetryCount = 0;

    public function connect(): void
    {
        if ($this->_conn != null) return;
        $this->_conn = new PDO($this->dsn, $this->username, $this->password, $this->options);

        /**
         * PDO 特性设置
         * @link https://www.php.net/manual/zh/pdo.setattribute.php
         */

        // 设置为 true 强制以字符串方式对待所有的值, 默认值 false
        // *** 目前MYSQL在使用模拟的预处理语句时候, 会导致查询结果所有字段值都会返回字符串型, php8.1 修复这个问题, 修复后查询结果的字段值可以与字段类型保持一致
        // 可以通过启用 ATTR_STRINGIFY_FETCHES 设置恢复以前的行为
        // @link https://www.php.net/manual/zh/migration81.incompatible.php#migration81.incompatible.pdo.mysql
        // $this->_conn->setAttribute(\PDO::ATTR_STRINGIFY_FETCHES, false);

        // PDO::ATTR_EMULATE_PREPARES 启用或禁用预处理语句的模拟。默认为 true
        // 有些驱动不支持或有限度地支持本地预处理。使用此设置强制PDO总是模拟预处理语句（如果为 true ），或试着使用本地预处理语句（如果为 false）。
        // 如果驱动不能成功预处理当前查询，它将总是回到模拟预处理语句上。 需要 bool 类型。
        // *** 目前MYSQL在使用模拟的预处理语句时候, 会导致查询结果所有字段值都会返回字符串型, php8.1 修复这个问题, 修复后查询结果的字段值可以与字段类型保持一致
        $this->_conn->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

        // PDO::ATTR_ORACLE_NULLS （在所有驱动中都可用，不仅限于Oracle）： 转换 NULL 和空字符串。
        // PDO::NULL_NATURAL: 不转换。
        // PDO::NULL_EMPTY_STRING： 将空字符串转换成 null。
        // PDO::NULL_TO_STRING: 将 NULL 转换成空字符串。
        $this->_conn->setAttribute(PDO::ATTR_ORACLE_NULLS, PDO::NULL_TO_STRING);

        // PDO::ATTR_ERRMODE：错误报告。
        // PDO::ERRMODE_SILENT： 仅设置错误代码。
        // PDO::ERRMODE_WARNING: 引发 E_WARNING 错误
        // PDO::ERRMODE_EXCEPTION: 抛出 exceptions 异常。
        $this->_conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

        // PDO::ATTR_CASE：强制列名为指定的大小写。
        // PDO::CASE_LOWER：强制列名小写。
        // PDO::CASE_NATURAL：保留数据库驱动返回的列名。
        // PDO::CASE_UPPER：强制列名大写。
        $this->_conn->setAttribute(PDO::ATTR_CASE, PDO::CASE_NATURAL);
    }

    public function close(): void
    {
        $this->_conn = null;
    }

    /**
     * 开启事务
     *
     * @return bool
     */
    public function transBegin(): bool
    {
        $this->connect();
        return $this->_conn->beginTransaction();
    }

    /**
     * 提交事务
     *
     * @return bool
     */
    public function transCommit(): bool
    {
        $this->connect();
        return $this->_conn->commit();
    }

    /**
     * 回滚事务
     *
     * @return bool
     */
    public function transRollBack(): bool
    {
        $this->connect();
        return $this->_conn->rollBack();
    }

    /**
     * 取最后插入数据编号
     *
     * @return int
     */
    public function getLastInsertId(): int
    {
        $this->connect();
        return intval($this->_conn->lastInsertId());
    }

    /**
     * 执行查询
     *
     * @param string $sql
     * @param array $params
     * @param array $prepareOptions
     *
     * @return PDOStatement
     */
    public function query(string $sql, array $params = [], array $prepareOptions = []): PDOStatement
    {
        $this->connect();
        try {
            $cmd = $this->_conn->prepare($sql, $prepareOptions);
            $cmd->execute($params);
            $this->_queryRetryCount = 0;
            return $cmd;
        } catch (\PDOException $ex) {
            if (($ex->errorInfo[1] == 2006 || $ex->errorInfo[1] == 2013) && ++$this->_queryRetryCount <= 3) {
                $this->close();
                return $this->query($sql, $params);
            }

            $paramsLog = json_encode($params, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE);
            Logger::get(static::class)->error("query fail: \r\nsql: $sql \r\nparams: $paramsLog", $ex);
            throw $ex;
        }
    }
}
